package editor.animation;

import animation.*;
import com.crunch.math.Vector2f;
import com.crunch.math.Vector4f;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Enumeration;
import java.util.HashMap;

public class SkeletonPanel extends EditorPanel {
	private JPanel skeletonControlsPanel;
	private JButton addBoneButton;
	private JButton removeBoneButton;
	private JTextField nameField;
	private JTree boneTree;

	private DefaultTreeModel model;
	private JPanel boneControlsPanel;
	private JSpinner lengthSpinner;
	private JSpinner thicknessSpinner;
	private JSpinner depthSpinner;
	private JSpinner translationXSpinner;
	private JSpinner translationYSpinner;
	private JSpinner rotationSpinner;
	private JSpinner scaleXSpinner;
	private JSpinner scaleYSpinner;

	private boolean performCallbacks = true;

	private static final Vector4f UNSELECTED_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
	private static final Vector4f SELECTED_COLOR = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f);

	@Override
	public String getTabName() {
		return "Skeleton";
	}

	public SkeletonPanel(AnimationEditor editor) {
		this.editor = editor;
		GroupLayout topLayout = new GroupLayout(this);
		setLayout(topLayout);
		topLayout.setAutoCreateGaps(true);
		topLayout.setAutoCreateContainerGaps(true);

		// skeleton controls/tree
		// bone controls

		skeletonControlsPanel = new JPanel();
		skeletonControlsPanel.setBorder(BorderFactory.createEtchedBorder());
		{
			GroupLayout layout = new GroupLayout(skeletonControlsPanel);
			skeletonControlsPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			addBoneButton = new JButton("Add bone");
			addBoneButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (SkeletonPanel.this.editor) {
						addBone();
					}
				}
			});

			removeBoneButton = new JButton("Remove bone");
			removeBoneButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (SkeletonPanel.this.editor) {
						removeBone();
					}
				}
			});

			JLabel nameLabel = new JLabel("Name:", JLabel.TRAILING);
			nameField = new JTextField();
			nameField.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (SkeletonPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeBoneProperties();
					}
				}
			});
			nameField.addFocusListener(new FocusListener() {
				@Override
				public void focusGained(FocusEvent e) {}

				@Override
				public void focusLost(FocusEvent e) {
					synchronized (SkeletonPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeBoneProperties();
					}
				}
			});
			nameLabel.setLabelFor(nameField);

			boneTree = new JTree(new DefaultMutableTreeNode("skeleton"));
			model = (DefaultTreeModel) boneTree.getModel();
			boneTree.setShowsRootHandles(true);
			boneTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
			boneTree.addTreeSelectionListener(new TreeSelectionListener() {
				@Override
				public void valueChanged(TreeSelectionEvent e) {
					synchronized (SkeletonPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}

						TreePath path = e.getNewLeadSelectionPath();
						DefaultMutableTreeNode node = (path == null || path.getLastPathComponent() == model.getRoot()) ?
								null : (DefaultMutableTreeNode) path.getLastPathComponent();
						int sel;
						if (node == null) {
							sel = -1;
						} else {
							sel = SkeletonPanel.this.editor.skeleton.getBoneIndex(node.toString());
						}

						storeBoneProperties();
						boneSelection = sel;
						setBoneControlsEnabled(sel >= 0);
						if (sel >= 0) {
							loadBoneProperties();
						}

						updateBoneColors();
					}
				}
			});
			JScrollPane boneTreeScroll = new JScrollPane(boneTree);

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(addBoneButton)
							.addComponent(removeBoneButton)
							.addComponent(nameLabel)
							.addComponent(nameField))
					.addComponent(boneTreeScroll));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(addBoneButton)
							.addComponent(removeBoneButton)
							.addComponent(nameLabel)
							.addComponent(nameField))
					.addComponent(boneTreeScroll));
		}

		boneControlsPanel = new JPanel();
		boneControlsPanel.setBorder(BorderFactory.createEtchedBorder());
		{
			GroupLayout layout = new GroupLayout(boneControlsPanel);
			boneControlsPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			ChangeListener changeListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					synchronized (SkeletonPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeBoneProperties();
					}
				}
			};

			final int MIN_TEXT_WIDTH = 64;

			JLabel lengthLabel = new JLabel("Length:", JLabel.TRAILING);
			lengthSpinner = new JSpinner(new SpinnerNumberModel(new Float(1.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			lengthSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, lengthSpinner.getMinimumSize().height));
			lengthSpinner.addChangeListener(changeListener);
			lengthLabel.setLabelFor(lengthSpinner);

			JLabel thicknessLabel = new JLabel("Thickness:", JLabel.TRAILING);
			thicknessSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.25f), new Float(0.0f), new Float(100000.0f), new Float(0.1f)));
			thicknessSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, thicknessSpinner.getMinimumSize().height));
			thicknessSpinner.addChangeListener(changeListener);
			thicknessLabel.setLabelFor(thicknessSpinner);

			JLabel depthLabel = new JLabel("Depth:", JLabel.TRAILING);
			depthSpinner = new JSpinner(new SpinnerNumberModel(0, Integer.MIN_VALUE, Integer.MAX_VALUE, 1));
			depthSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, depthSpinner.getMinimumSize().height));
			depthSpinner.addChangeListener(changeListener);
			depthLabel.setLabelFor(depthSpinner);

			JLabel translationLabel = new JLabel("Translation:", JLabel.TRAILING);
			translationXSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			translationXSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, translationXSpinner.getMinimumSize()
					.height));
			translationXSpinner.addChangeListener(changeListener);
			translationYSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			translationYSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, translationYSpinner.getMinimumSize().height));
			translationYSpinner.addChangeListener(changeListener);
			translationLabel.setLabelFor(translationXSpinner);

			JLabel rotationLabel = new JLabel("Rotation:", JLabel.TRAILING);
			rotationSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
			rotationSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, rotationSpinner.getMinimumSize().height));
			rotationSpinner.addChangeListener(changeListener);
			rotationLabel.setLabelFor(rotationSpinner);

			JLabel scaleLabel = new JLabel("Scale:", JLabel.TRAILING);
			scaleXSpinner = new JSpinner(new SpinnerNumberModel(new Float(1.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			scaleXSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, scaleXSpinner.getMinimumSize().height));
			scaleXSpinner.addChangeListener(changeListener);
			scaleYSpinner = new JSpinner(new SpinnerNumberModel(new Float(1.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			scaleYSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, scaleYSpinner.getMinimumSize().height));
			scaleYSpinner.addChangeListener(changeListener);
			scaleLabel.setLabelFor(scaleXSpinner);

			layout.setHorizontalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(lengthLabel)
							.addComponent(thicknessLabel)
							.addComponent(depthLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(lengthSpinner)
							.addComponent(thicknessSpinner)
							.addComponent(depthSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(translationLabel)
							.addComponent(rotationLabel)
							.addComponent(scaleLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addGroup(layout.createSequentialGroup()
									.addComponent(translationXSpinner)
									.addComponent(translationYSpinner))
							.addComponent(rotationSpinner)
							.addGroup(layout.createSequentialGroup()
									.addComponent(scaleXSpinner)
									.addComponent(scaleYSpinner))));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(lengthLabel)
							.addComponent(lengthSpinner)
							.addComponent(translationLabel)
							.addComponent(translationXSpinner)
							.addComponent(translationYSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(thicknessLabel)
							.addComponent(thicknessSpinner)
							.addComponent(rotationLabel)
							.addComponent(rotationSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(depthLabel)
							.addComponent(depthSpinner)
							.addComponent(scaleLabel)
							.addComponent(scaleXSpinner)
							.addComponent(scaleYSpinner)));
		}

		topLayout.setHorizontalGroup(topLayout.createParallelGroup(GroupLayout.Alignment.CENTER)
				.addComponent(skeletonControlsPanel)
				.addComponent(boneControlsPanel));
		topLayout.setVerticalGroup(topLayout.createSequentialGroup()
				.addComponent(skeletonControlsPanel)
				.addComponent(boneControlsPanel));
	}

	@Override
	public void select() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// clear the tree and rebuild it up
			boneTree.clearSelection();
			DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
			root.removeAllChildren();
			model.reload();

			for (int i = 0; i < editor.skeleton.getRootBoneCount(); ++i) {
				addTreeNodes(editor.skeleton.getRootBone(i), root);
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		// expand the whole tree - note, this is probably n^2 due to the getRowCount() call in the loop
		for (int i = 0; i < boneTree.getRowCount(); ++i) {
			boneTree.expandRow(i);
		}

		boneSelection = -1;
		setBoneControlsEnabled(false);

		constructCharacterInstance();
	}

	@Override
	public void deselect() {}

	@Override
	public void close() {}

	private void addTreeNodes(int idx, DefaultMutableTreeNode parent) {
		Skeleton.Bone bone = editor.skeleton.getBone(idx);

		DefaultMutableTreeNode node = new DefaultMutableTreeNode(bone.getName());
		model.insertNodeInto(node, parent, parent.getChildCount());

		for (int i = 0; i < bone.getChildCount(); ++i) {
			addTreeNodes(bone.getChild(i), node);
		}
	}

	private int boneSelection;  // ID of bone selected within skeleton

	private void storeBoneProperties() {
		if (boneSelection < 0) {
			return;
		}
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// name
			String name = nameField.getText();
			if (name.isEmpty() || editor.skeleton.getBoneIndex(name) >= 0) {
				// revert to current bone name
				nameField.setText(editor.skeleton.getBone(boneSelection).getName());
			} else {
				// set the bone's name
				String oldName = editor.skeleton.getBone(boneSelection).getName();
				editor.skeleton.setBoneName(boneSelection, name);
				DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
				@SuppressWarnings("unchecked")
				Enumeration<DefaultMutableTreeNode> enumeration = root.preorderEnumeration();
				enumeration.nextElement(); // skip root
				while (enumeration.hasMoreElements()) {
					DefaultMutableTreeNode node = enumeration.nextElement();
					if (node.toString().equals(oldName)) {
						model.valueForPathChanged(new TreePath(node.getPath()), name);
						boneTree.repaint();
						break;
					}
				}
			}

			Skeleton.Bone bone = editor.skeleton.getBone(boneSelection);
			bone.setLength((Float) lengthSpinner.getValue());
			bone.setThickness((Float) thicknessSpinner.getValue());
			editor.skeleton.setBoneDepth(boneSelection, (Integer) depthSpinner.getValue());
			bone.setTranslation(new Vector2f(
					(Float) translationXSpinner.getValue(),
					(Float) translationYSpinner.getValue()));

			float rotation = (Float) rotationSpinner.getValue();
			rotation %= 360.0f;
			if (rotation < 0.0f) {
				rotation += 360.0f;
			}
			bone.setRotation((float) Math.toRadians(rotation));
			rotationSpinner.setValue(rotation);

			bone.setScale(new Vector2f(
					(Float) scaleXSpinner.getValue(),
					(Float) scaleYSpinner.getValue()));
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		constructCharacterInstance();
	}

	private void loadBoneProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Skeleton.Bone bone = editor.skeleton.getBone(boneSelection);
			nameField.setText(bone.getName());
			lengthSpinner.setValue(bone.getLength());
			thicknessSpinner.setValue(bone.getThickness());
			depthSpinner.setValue(bone.getDepth());
			translationXSpinner.setValue(bone.getTranslation().x());
			translationYSpinner.setValue(bone.getTranslation().y());
			rotationSpinner.setValue((float) Math.toDegrees(bone.getRotation()));
			scaleXSpinner.setValue(bone.getScale().x());
			scaleYSpinner.setValue(bone.getScale().y());

			// set selection bone color
			for (int i = 0; i < editor.boneColors.length; ++i) {
				editor.boneColors[i] = (boneSelection == i) ? SELECTED_COLOR : UNSELECTED_COLOR;
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setBoneControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			nameField.setEnabled(e);
			removeBoneButton.setEnabled(e);
			lengthSpinner.setEnabled(e);
			thicknessSpinner.setEnabled(e);
			depthSpinner.setEnabled(e);
			translationXSpinner.setEnabled(e);
			translationYSpinner.setEnabled(e);
			rotationSpinner.setEnabled(e);
			scaleXSpinner.setEnabled(e);
			scaleYSpinner.setEnabled(e);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void addBone() {
		int val = 0;
		// find unused bone name
		boolean used = true;
		while (used) {
			if (editor.skeleton.getBoneIndex("bone" + val) < 0) {
				used = false;
			} else {
				++val;
			}
		}

		String name = "bone" + val;
		DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(name);

		// add to child
		DefaultMutableTreeNode node;
		if (boneSelection < 0) {
			// add to root
			node = (DefaultMutableTreeNode) model.getRoot();
			editor.skeleton.addBone(name, 1.0f, 0.125f, new Vector2f(0.0f, 0.0f), 0.0f, new Vector2f(1.0f, 1.0f), 0);
		} else {
			node = (DefaultMutableTreeNode) boneTree.getSelectionPath().getLastPathComponent();
			String parent = node.toString();
			int parentIdx = editor.skeleton.getBoneIndex(parent);
			Skeleton.Bone parentBone = editor.skeleton.getBone(parentIdx);
			editor.skeleton.addBone(name, parentIdx, 1.0f, 0.125f, new Vector2f(parentBone.getLength(), 0.0f), 0.0f, new Vector2f(1.0f, 1.0f), 0);
		}

		model.insertNodeInto(newNode, node, node.getChildCount());
		constructCharacterInstance();
		boneTree.setSelectionPath(new TreePath(newNode.getPath()));
	}

	private void removeBone() {
		int sel = boneSelection;

		DefaultMutableTreeNode node = (DefaultMutableTreeNode) boneTree.getSelectionPath().getLastPathComponent();
		boneTree.clearSelection();

		// go through each animation and remove the bone
		for (int i = 0; i < editor.animations.size(); ++i) {
			removeBonesFromAnimation(sel, editor.animations.get(i));
		}
		// go through each skin and remove the bone
		for (int i = 0; i < editor.skins.size(); ++i) {
			removeBonesFromSkin(sel, editor.skins.get(i));
		}
		editor.skeleton.removeBone(sel);
		((DefaultTreeModel) boneTree.getModel()).removeNodeFromParent(node);

		constructCharacterInstance();
	}

	private void removeBonesFromAnimation(int index, Animation animation) {
		Skeleton.Bone bone = editor.skeleton.getBone(index);
		if (animation.getBone(bone.getName()) != null) {
			animation.removeAnimatedBone(bone.getName());
		}
		for (int i = 0; i < bone.getChildCount(); ++i) {
			removeBonesFromAnimation(bone.getChild(i), animation);
		}
	}

	private void removeBonesFromSkin(int index, Skin skin) {
		Skeleton.Bone bone = editor.skeleton.getBone(index);
		if (skin.getBone(bone.getName()) != null) {
			skin.removeSkinnedBone(bone.getName());
		}
		for (int i = 0; i < bone.getChildCount(); ++i) {
			removeBonesFromSkin(bone.getChild(i), skin);
		}
	}

	public void constructCharacterInstance() {
		// no need to construct animations for skeleton modification
		//HashMap<String, Animation> animMap = new HashMap<String, Animation>();
		//for (int i = 0; i < editor.animations.size(); ++i) {
		//	animMap.put(editor.animationNames.get(i), editor.animations.get(i));
		//}
		editor.character = new animation.Character(
				editor.skeleton,
				new AnimationTree(),            // empty tree
				new AnimationStateMachine(),    // empty state machine
				new HashMap<String, Animation>());
		editor.state = editor.character.createCharacterState();
		editor.evaluator.evaluate(editor.state);
		editor.boneColors = new Vector4f[editor.skeleton.getBoneCount()];
		updateBoneColors();
	}

	private void updateBoneColors() {
		for (int i = 0; i < editor.boneColors.length; ++i) {
			editor.boneColors[i] = UNSELECTED_COLOR;
		}
		if (boneTree.getSelectionPath() != null && boneTree.getSelectionPath().getLastPathComponent() != model.getRoot()) {
			DefaultMutableTreeNode selNode = (DefaultMutableTreeNode) boneTree.getSelectionPath().getLastPathComponent();
			int index = editor.skeleton.getBoneIndex(selNode.toString());
			editor.boneColors[index] = SELECTED_COLOR;
		}
	}

	private final AnimationEditor editor;

	// PREVIEW FUNCTIONS

	@Override
	public void selectBoneInPreview(int boneIndex) {
		synchronized (editor) {
			if (boneIndex < 0) {
				// deselect
				boneTree.clearSelection();
			} else {
				// make sure the bone still exists
				if (boneIndex >= editor.skeleton.getBoneCount())
					return;

				String boneName = editor.skeleton.getBone(boneIndex).getName();
				// find the bone in the list
				@SuppressWarnings("unchecked")
				Enumeration<DefaultMutableTreeNode> enumeration =
						((DefaultMutableTreeNode) model.getRoot()).preorderEnumeration();
				enumeration.nextElement(); // skip root
				while (enumeration.hasMoreElements()) {
					DefaultMutableTreeNode node = enumeration.nextElement();
					if (node.toString().equals(boneName)) {
						boneTree.setSelectionPath(new TreePath(node.getPath()));
						break;
					}
				}
			}
		}
	}

	@Override
	public void translateInPreview(Vector2f translation) {
		synchronized (editor) {
			if (boneSelection < 0) {
				return;
			}
			translationXSpinner.setValue(translation.x());
			translationYSpinner.setValue(translation.y());
		}
	}

	@Override
	public void finishTranslateInPreview() {
	}

	@Override
	public void rotateInPreview(float rotation) {
		synchronized (editor) {
			if (boneSelection < 0) {
				return;
			}
			rotationSpinner.setValue((float) Math.toDegrees(rotation));
		}
	}

	@Override
	public void finishRotateInPreview() {
	}

	@Override
	public void scaleInPreview(Vector2f scale) {
		synchronized (editor) {
			if (boneSelection < 0) {
				return;
			}
			scaleXSpinner.setValue(scale.x());
			scaleYSpinner.setValue(scale.y());
		}
	}

	@Override
	public void finishScaleInPreview() {
	}

	@Override
	public Vector2f getBoneTranslation(int boneIndex) {
		synchronized (editor) {
			if (boneIndex < 0 || boneIndex >= editor.skeleton.getBoneCount()) {
				return null;
			}
			return editor.skeleton.getBone(boneIndex).getTranslation();
		}
	}

	@Override
	public float getBoneRotation(int boneIndex) {
		synchronized (editor) {
			if (boneIndex < 0 || boneIndex >= editor.skeleton.getBoneCount()) {
				return 0.0f;
			}
			return editor.skeleton.getBone(boneIndex).getRotation();
		}
	}

	@Override
	public Vector2f getBoneScale(int boneIndex) {
		synchronized (editor) {
			if (boneIndex < 0 || boneIndex >= editor.skeleton.getBoneCount()) {
				return null;
			}
			return editor.skeleton.getBone(boneIndex).getScale();
		}
	}

	public void enableBoneInAnimationInPreview() {}
	public void addKeyframeInPreview() {}
	public void prevKeyframeInPreview(boolean first) {}
	public void nextKeyframeInPreview(boolean last) {}
	public void frameBeginInPreview() {}
}
